﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

namespace KMS.Core
{
   
    public static class DapperExtensions
    {
        public static bool IsSqlTrace
        {
            get { return _isSqlTrace; }
            set { _isSqlTrace = value; }
        }
        private static bool _isSqlTrace = true;
        private static readonly TraceSource traceSource = new TraceSource("Dapper.Aggregater", SourceLevels.Verbose);
        internal static void WriteLine(string message)
        {
            if (!IsSqlTrace) return;
            traceSource.TraceEvent(TraceEventType.Verbose, 0, message);
        }
        internal static void WriteLine(string format, params object[] args)
        {
            if (!IsSqlTrace) return;
            traceSource.TraceEvent(TraceEventType.Verbose, 0, format, args);
        }

        //poco pattern
        // If the class does not implement interface(IContainerHolder), I embed interface dynamically using TypeBuilder.
        public static IEnumerable<T> QueryWith<T>(this IDbConnection cnn, Query<T> query,
            IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, int splitLength = 100, int queryOptimizerLevel = 10)
        {
            return cnn.QueryWith(query as QueryImp, transaction, buffered, commandTimeout, splitLength, queryOptimizerLevel).OfType<T>();
        }
        public static IEnumerable<object> QueryWith(this IDbConnection cnn, QueryImp query,
            IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, int splitLength = 100, int queryOptimizerLevel = 10)
        {
            query.Ensure(splitLength, queryOptimizerLevel);

            var oldType = query.RootType;
            var newParentType = ILGeneratorUtil.IsInjected(oldType) ? ILGeneratorUtil.InjectionInterfaceWithProperty(oldType) : oldType;

            WriteLine(query.Sql);
            var rows = cnn.Query(newParentType, query.Sql, query.Parameters, transaction, buffered, commandTimeout);
            if (rows != null && rows.Any())
            {
                var command = new CommandDefinition(query.SqlIgnoreOrderBy, query.Parameters, transaction, commandTimeout, null, buffered ? CommandFlags.Buffered : CommandFlags.None);
                LoadWith(cnn, command, newParentType, query.Relations.ToArray(), rows);
            }
            return rows;
        }

        public static T ScalarWith<T>(this IDbConnection cnn, QueryImp query, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            WriteLine(query.Sql);
            return cnn.ExecuteScalar<T>(query.Sql, query.Parameters, transaction, commandTimeout, CommandType.Text);
        }

        private static void LoadWith(IDbConnection cnn, CommandDefinition command, Type t, RelationAttribute[] atts, System.Collections.IEnumerable roots)
        {
            var rootAtts = atts.Where(x => !x.Loaded && x.ParentType == t).ToArray();
            if (!rootAtts.Any()) return;

            var rootDataStore = new DataStore();
            var enumerator = roots.GetEnumerator();
            var hasValue = enumerator.MoveNext();
            while (hasValue)
            {
                var value = enumerator.Current;
                var holder = value as IContainerHolder;
                if (holder != null)
                {
                    holder.Container = new DataContainer(value, rootAtts, rootDataStore);
                }
                foreach (var att in rootAtts)
                {
                    att.DataAdapter.AssignDataParameter(value);
                }
                hasValue = enumerator.MoveNext();
            }

            foreach (var att in rootAtts)
            {
                var list = att.DataAdapter.Fill(cnn, command, atts);
                att.Loaded = true;
                rootDataStore.Add(att.Key, list);
                if (list.Count != 0)
                {
                    LoadWith(cnn, command, att.ChildType, atts, list);
                }
            }
        }
        public static int UpdateQuery<T>(this IDbConnection cnn, UpdateQuery<T> query,
            IDbTransaction transaction = null, int? commandTimeout = null)
        {
            WriteLine(query.UpdateClauses);
            return cnn.Execute(query.UpdateClauses, query.Parameters, transaction, commandTimeout);
        }

        public static int DeleteQuery<T>(this IDbConnection cnn, Query<T> query,
            IDbTransaction transaction = null, int? commandTimeout = null, bool isRootOnly = true)
        {
            if (!isRootOnly)
            {
                query.Ensure(injectionDynamicType: false);
                var rootType = typeof(T);
                var rootView = string.Format("SELECT {0} FROM {1} {2}", query.SelectClause, query.TableClause, query.WhereClause);
                var atts = query.Relations.ToList();
                var list = new List<DeleteQueryObject>();
                foreach (var each in query.Relations)
                {
                    if (each.ParentType == typeof(T))
                    {
                        atts.Remove(each);
                        RecursiveDeleteQuery(atts, list, new DeleteQueryObject(each, rootView, 0));
                    }
                }
                foreach (var each in list.OrderByDescending(x => x.NestLevel))
                {
                    WriteLine(each.DeleteClause);
                    cnn.Execute(each.DeleteClause, query.Parameters, transaction, commandTimeout);
                }
            }
            var rootDeleteSql = string.Format("DELETE FROM {0} {1}", query.TableClause, query.WhereClause);
            return cnn.Execute(rootDeleteSql, query.Parameters, transaction, commandTimeout);
        }
        private static void RecursiveDeleteQuery(List<RelationAttribute> atts, List<DeleteQueryObject> items, DeleteQueryObject rd)
        {
            items.Add(rd);
            var arr = atts.ToArray();
            foreach (var each in arr)
            {
                if (each.ParentType == rd.Criteria.Att.ChildType)
                {
                    atts.Remove(each);
                    RecursiveDeleteQuery(atts, items, new DeleteQueryObject(each, rd.View, rd.NestLevel + 1));
                }
            }
        }
 
        public static Int64 Count<T>(this IDbConnection cnn, Query<T> query)
        {
            var sql = string.Format("SELECT COUNT(1) FROM ({0}) T", query.SqlIgnoreOrderBy);
            WriteLine(sql);
            return cnn.ExecuteScalar<Int64>(sql, query.Parameters);
        }
        public static TResult Count<TResult>(this IDbConnection cnn, QueryImp query)
        {
            var sql = string.Format("SELECT COUNT(1) FROM ({0}) T", query.SqlIgnoreOrderBy);
            WriteLine(sql);
            return cnn.ExecuteScalar<TResult>(sql, query.Parameters);
        }

        public static int InsertEntity<T>(this IDbConnection cnn, T entity, IDbTransaction transaction = null, int? commandTimeout = null, bool throwValidateError = true)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            var type = typeof(T);
            //var table = type.GetTableName();
            //var columns = type.GetSelectClause().Where(c=>!c.Identity).ToList();
            //var accessors = PropertyAccessorImp.ToPropertyAccessors(type).ToDictionary(x => x.Name);
            //var validators = AttributeUtil.Find<EntityValidateAttribute>(type);
            var hasIndentity = type.HaveIdentity();

            //foreach (var validator in validators)
            //{
            //    var result = validator.GetError(entity);
            //    if (result != null)
            //    {
            //        if (throwValidateError)
            //        {
            //            throw result;
            //        }
            //    }
            //}

            var sql = type.GetInsertSql();
            if (hasIndentity)
            {
                sql += " select cast(scope_identity() as int) ";
            }
            WriteLine(sql);
            if (hasIndentity)
            {
               return cnn.Query<int>(sql, entity, transaction,true,commandTimeout).SingleOrDefault(); 
            }
            else
            {
                return cnn.Execute(sql, entity, transaction, commandTimeout); 
            } 
        }
        public static void InsertEntityList<T>(this IDbConnection cnn, IEnumerable<T> entities,
            IDbTransaction transaction = null, int? commandTimeout = null, bool throwValidateError = true)
        {
            var sql = typeof(T).GetInsertSql();
            cnn.Execute(sql, entities, transaction, commandTimeout);
            //foreach (var each in entities)
            //{
            //    InsertEntity(cnn, each, transaction, commandTimeout, throwValidateError);
            //}
        }

        public static int UpdateEntity(this IDbConnection cnn, object entity,
            IDbTransaction transaction = null, int? commandTimeout = null, bool throwValidateError = true)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            var type = entity.GetType();
         
            var columns = type.GetSelectClause();
            if (!columns.Any(x => x.IsPrimaryKey || x.Identity))
                throw new System.Data.MissingPrimaryKeyException("should have a primary key.");
  
            var sql = type.GetUpdateEntitySql();
            WriteLine(sql);
            var count = cnn.Execute(sql, entity, transaction, commandTimeout);
            return count;
        }
        public static void UpdateEntity<T>(this IDbConnection cnn, IEnumerable<T> entities,
            IDbTransaction transaction = null, int? commandTimeout = null, bool throwValidateError = true)
        {
            foreach (var each in entities)
            {
                UpdateEntity(cnn, each, transaction, commandTimeout, throwValidateError);
            }
        }
        public static void DeleteEntity(this IDbConnection cnn, object entity,
            IDbTransaction transaction = null, int? commandTimeout = null, bool throwValidateError = true)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            var type = entity.GetType();
            var table = type.GetTableName();
            var columns = type.GetSelectClause();
            if (!columns.Any(x => x.IsPrimaryKey))
                throw new System.Data.MissingPrimaryKeyException("should have a primary key.");

            var accessors = PropertyAccessorImp.ToPropertyAccessors(type).ToDictionary(x => x.Name);
            var validators = AttributeUtil.Find<EntityValidateAttribute>(type);
          
            foreach (var validator in validators)
            {
                var result = validator.GetError(entity);
                if (result != null)
                {
                    if (throwValidateError)
                    {
                        throw result;
                    }
                }
            }

            var dic = new Dictionary<string, object>();
            var whereList = new List<SetClausesHolder>();
            var index = 0;
            foreach (var column in columns.Where(x => (x.IsPrimaryKey || x.Identity) && !x.Ignore))
            {
                var whereClauses = new SetClausesHolder(column.Name, accessors[column.PropertyInfoName].GetValue(entity), ++index);
                dic[whereClauses.Placeholder] = whereClauses.Value;
                whereList.Add(whereClauses);
            }
            var sql = string.Format("DELETE FROM {0} WHERE {1}", table,
                        string.Join(" AND ", whereList.Select(x => x.Clauses)));
            WriteLine(sql);
            var count = cnn.Execute(sql, dic, transaction, commandTimeout);
            if (count != 1)
                throw new System.Data.DBConcurrencyException("entity has already been updated by another user.");
        }
        public static void DeleteEntity<T>(this IDbConnection cnn, IEnumerable<T> entities,
           IDbTransaction transaction = null, int? commandTimeout = null, bool throwValidateError = true)
        {
            foreach (var each in entities)
            {
                DeleteEntity(cnn, each, transaction, commandTimeout, throwValidateError);
            }
        }
    }
}